package springdao.support;

import static java.sql.Types.DECIMAL;
import static java.sql.Types.NUMERIC;

import java.io.Serializable;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Properties;

import javax.persistence.Enumerated;
import org.hibernate.annotations.Type;
import org.hibernate.annotations.TypeDef;
import org.hibernate.HibernateException;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.usertype.ParameterizedType;
import org.hibernate.usertype.UserType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 *&#47;&#47;Build customed Enum, for example: column values like this &quot;C&quot;:Customer、&quot;V&quot;:Vender、&quot;W&quot;:WareHousing…,<br/>
 *&#47;&#47;and we careate:<br/>
 *&#47;&#47;建立自訂類型，例如在資料庫數據&quot;C&quot;代表客戶、&quot;V&quot;代表供應商、&quot;W&quot;代表倉儲業者…，<br/>
 *&#47;&#47;此時先建立<br/>
 *&nbsp;public enum SupplyChainMember{<br/>
 *&nbsp;&nbsp;&nbsp;&#47;**<br/>
 *&nbsp;&nbsp;&nbsp;&nbsp;*&nbsp;Customer(客戶)<br/>
 *&nbsp;&nbsp;&nbsp;&nbsp;*&#47;<br/>
 *&nbsp;&nbsp;&nbsp;&nbsp;CUSTOMER,<br/>
 *&nbsp;&nbsp;&nbsp;&#47;**<br/>
 *&nbsp;&nbsp;&nbsp;&nbsp;*&nbsp;Vender(供應商)<br/>
 *&nbsp;&nbsp;&nbsp;&nbsp;*&#47;<br/>
 *&nbsp;&nbsp;&nbsp;&nbsp;VENDOR,<br/>
 *&nbsp;&nbsp;&nbsp;&#47;**<br/>
 *&nbsp;&nbsp;&nbsp;&nbsp;*&nbsp;WareHousing(倉儲業者)<br/>
 *&nbsp;&nbsp;&nbsp;&nbsp;*&#47;<br/>
 *&nbsp;&nbsp;&nbsp;&nbsp;WAREHOUSE<br/>
 *&nbsp;}<br/>
 *&#47;&#47;Furthermore<br/>
 *&#47;&#47;然後再建立<br/>
 *&nbsp;public&nbsp;class&nbsp;SupplyChainMemberType&nbsp;extends&nbsp;EnumUserType<SupplyChainMember,&nbsp;String>&nbsp;{<br/><br/>
 *&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;@Override<br/>
 *&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;protected&nbsp;String&nbsp;getOriginalValue(SupplyChainMember&nbsp;enumInstance)&nbsp;{<br/>
 *&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if&nbsp;(SupplyChainMember.CUSTOMER.equals(enumInstance))&nbsp;{<br/>
 *&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return&nbsp;"C";<br/>
 *&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp;else&nbsp;if&nbsp;(SupplyChainMember.VENDOR.equals(enumInstance))&nbsp;{<br/>
 *&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return&nbsp;"V";<br/>
 *&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp;else&nbsp;if&nbsp;(SupplyChainMember.WAREHOUSE.equals(enumInstance))&nbsp;{<br/>
 *&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return&nbsp;"W";<br/>
 *&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp;else&nbsp;{<br/>
 *&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return&nbsp;null;<br/>
 *&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}<br/>
 *&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}<br/><br/>
 *&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;@Override<br/>
 *&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;protected&nbsp;SupplyChainMember&nbsp;getEnumInstance(String&nbsp;original)&nbsp;{<br/>
 *&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if&nbsp;("C".equals(original))&nbsp;{<br/>
 *&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return&nbsp;SupplyChainMember.CUSTOMER;<br/>
 *&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp;else&nbsp;if&nbsp;("V".equals(original))&nbsp;{<br/>
 *&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return&nbsp;SupplyChainMember.VENDOR;<br/>
 *&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp;else&nbsp;if&nbsp;("W".equals(original))&nbsp;{<br/>
 *&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return&nbsp;SupplyChainMember.WAREHOUSE;<br/>
 *&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp;else&nbsp;{<br/>
 *&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return&nbsp;null;<br/>
 *&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}<br/>
 *&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}<br/><br/>
 *&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;@Override<br/>
 *&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;public&nbsp;int&nbsp;getSqlType()&nbsp;{<br/>
 *&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return&nbsp;java.sql.Types.VARCHAR;<br/>
 *&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}<br/><br/>
 *&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;@Override<br/>
 *&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;public&nbsp;Class<SupplyChainMember>&nbsp;returnedClass()&nbsp;{<br/>
 *&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return&nbsp;SupplyChainMember.class;<br/>
 *&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}<br/>
 *&nbsp;}<br/><br/>
 *&#47;&#47;When introduce JPA annotation, usage:<br/>
 *&#47;&#47;若是使用JPA annotation，用法如下:<br/>
 *<dl>
 *  <dt>1.Define custom type:<br/>&nbsp;&nbsp;宣告自訂型態
 *  <dd>&#64;{@link TypeDef}(name = "supplyChainMember", defaultForType = SupplyChainMember.class, typeClass = SupplyChainMemberType.class)</dd>
 *  <dt>2.Annotate Class member<br/>&nbsp;&nbsp;標記成員
 *  <dd>public class Member ...{<br/>
 *&nbsp;&nbsp;&#64;{@link Type}(type = "supplyChainMember")<br/>
 *&nbsp;&nbsp;private SupplyChainMember userType = SupplyChainMember.CUSTOMER;<br/>
 *&nbsp;&nbsp;...<br/>   
 *}</dd>
 *</dl>
 *&#47;&#47;When Enum storage using VARCHAR or INT,you also can use{@link Enumerated @Enumerated} instead, for example：<br/>
 *&#47;&#47;若是您的Enum儲存在資料庫的型態是字串或是整數，也可以直接用{@link Enumerated @Enumerated}，上述的範例修改如下：<br/>
 *public enum SupplyChainMember{<br/>
 *&nbsp;&nbsp;CUSTOMER(&quot;C&quot;),<br/>
 *&nbsp;&nbsp;VENDOR(&quot;V&quot;),<br/>
 *&nbsp;&nbsp;WAREHOUSE(&quot;W&quot;);<br/>
 *&nbsp;&nbsp;private String rating;<br/> 
 *&nbsp;&nbsp;private Rating(String rating) {<br/> 
 *&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;this.rating = rating;<br/> 
 *&nbsp;&nbsp;}<br/>
 *&nbsp;&nbsp;&#47;&#47;The drawback is toString() direct return storage value, it may be not human readable.<br/>
 *&nbsp;&nbsp;&#47;&#47;If sql type is INT, change code to <span style="color:blue">public int getValue() { return value; }</span><br/>
 *&nbsp;&nbsp;&#47;&#47;缺點是在此toString()回傳了代表字串，少了解釋Enum值意義的地方<br/>
 *&nbsp;&nbsp;&#47;&#47;若是資料庫存的是整數，則必須改寫<span style="color:blue">public int getValue() { return value; }</span><br/>
 *&nbsp;&nbsp;&#64;Override<br/> 
 *&nbsp;&nbsp;public String toString() {<br/> 
 *&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return rating;<br/> 
 *&nbsp;&nbsp;}<br/> 
 *}<br/>
 *&nbsp;&nbsp;&#64;Enumerated(EnumType.STRING)<br/> 
 *&nbsp;&nbsp;private SupplyChainMember supplyChainMember;<br/> 
 *
 * @author Kent Yeh
 */
public abstract class EnumUserType<E extends Enum<E>, V> implements UserType, ParameterizedType {

    private static Logger logger = LoggerFactory.getLogger(EnumUserType.class);

    public EnumUserType() {
        super();
    }

    /**
     * Return starage value,for example: return  string &quot;W&quot; when enum instance is WAREHOUSE.<br/>
     * 將Enum 列舉物件轉換成對應的資料庫存值，如enumInstance為WAREHOUSE時，應回傳 &quot;W&quot;字串
     * @param enumInstance Enum Object(物件)
     * @return starage value(資料庫對應存值)
     */
    protected abstract V getOriginalValue(E enumInstance);

    /**
     * Return mapping {@link Enum} instance, for example:  return CUSTOMER enum when starage value is  &quot;C&quot;.<br/>
     * 將資料庫存值轉換成對應對{@link Enum}物件，如資料庫值為&quot;C&quot;時，應回傳CUSTOMER列舉物件
     * @param original 資料庫對應存值
     * @return 列舉物件
     */
    protected abstract E getEnumInstance(V original);

    @Override
    public void setParameterValues(Properties parameters) {
    }

    /**
     * It is nessary to return scale length if SQL Type is <a href="http://download.oracle.com/javase/7/docs/api/java/sql/Types.html#NUMERIC">NUMERIC</a>
     *  or <a href="http://download.oracle.com/javase/7/docs/api/java/sql/Types.html#DECIMAL">DECIMAL</a>(sound impossible).<br/>
     * SQL Type 若為 <a href="http://download.oracle.com/javase/7/docs/api/java/sql/Types.html#NUMERIC">NUMERIC</a>或是
     * <a href="http://download.oracle.com/javase/7/docs/api/java/sql/Types.html#DECIMAL">DECIMAL</a>時(雖然Enum不太可能用這兩種SqlType)，
     * 必須確定數值精度或長度
     * @return default return -1.(數值精度或長度，預設為-1)
     */
    protected int getScaleOrLength() {
        return -1;
    }

    public abstract int getSqlType();

    @Override
    public Object assemble(Serializable cached, Object owner)
            throws HibernateException {
        return cached;
    }

    @Override
    public Object deepCopy(Object value) throws HibernateException {
        return value;
    }

    @Override
    public Serializable disassemble(Object value) throws HibernateException {
        return (Serializable) value;
    }

    @Override
    public boolean equals(Object x, Object y) throws HibernateException {
        if (x == y) {
            return true;
        } else if (null == x || null == y) {
            return false;
        } else {
            return x.equals(y);
        }
    }

    @Override
    public int hashCode(Object x) throws HibernateException {
        return x.hashCode();
    }

    @Override
    public boolean isMutable() {
        return false;
    }

    @Override
    @SuppressWarnings("unchecked")
    public Object nullSafeGet(ResultSet resultSet, String[] names, SessionImplementor session, Object owner) throws HibernateException, SQLException {
        return resultSet.wasNull() ? null : getEnumInstance((V) resultSet.getObject(names[0]));
    }

    @Override
    public void nullSafeSet(PreparedStatement preparedStatement, Object enumObject, int index, SessionImplementor session) throws HibernateException, SQLException {
        if (null == enumObject) {
            preparedStatement.setNull(index, getSqlType());
        } else {
            try {
                E enumInstance = returnedClass().cast(enumObject);
                if (this.getScaleOrLength() > -1 && getSqlType() == DECIMAL && getSqlType() == NUMERIC) {
                    preparedStatement.setObject(index, getOriginalValue(enumInstance), getSqlType(), getScaleOrLength());
                } else {
                    preparedStatement.setObject(index, getOriginalValue(enumInstance), getSqlType());
                }
            } catch (ClassCastException e) {
                logger.error(e.getMessage(), e);
                preparedStatement.setNull(index, getSqlType());
            }
        }
    }

    @Override
    public Object replace(Object original, Object target, Object owner)
            throws HibernateException {
        return original;
    }

    /* 
     * Return ENUM class type.<br/>
     * 回傳包裝Enum的真實類別
     * @see org.hibernate.usertype.UserType#returnedClass()
     */
    @Override
    public abstract Class<E> returnedClass();

    @Override
    public int[] sqlTypes() {
        return new int[]{getSqlType()};
    }
}
